package com.chang.product.service.impl;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.chang.common.utils.PageUtils;
import com.chang.common.utils.Query;
import com.chang.product.entity.CategoryEntity;
import com.chang.product.mapper.CategoryDao;
import com.chang.product.service.CategoryBrandRelationService;
import com.chang.product.service.CategoryService;
import com.chang.product.utils.redis.RedisUtils;
import com.chang.product.vo.CatelogTwoVo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;

import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;


@Service("categoryService")
@Slf4j
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService {

    @Autowired
    CategoryBrandRelationService categoryBrandRelationService;


    // 本地缓存
    // private Map<String,Object> cacheMap = new ConcurrentHashMap<>();

    /*@Resource
    StringRedisTemplate redisTemplate;*/

    //@Autowiredm
    //CategoryDao categoryDao;

    //CategoryServiceImpl继承了ServiceImpl方法，而ServiceImpl方法本身就加入了CategoryDao的实现
    //ServiceImpl点进去就能看到泛型CategoryDao被声明为了baseMapper，而CategoryDao本身又继承了一个BaseMapper类（这个和声明的baseMapper不一样）
    //所以这里不用注入dao就能直接使用dao层的方法

    @Override
    public PageUtils queryPage(Map<String, Object> params) {
        IPage<CategoryEntity> page = this.page(
                new Query<CategoryEntity>().getPage(params),
                new QueryWrapper<>()
        );

        return new PageUtils(page);
    }

    @Override
    public List<CategoryEntity> listWithTree() {
        // 查出所有分类
        List<CategoryEntity> categoryList = baseMapper.selectList(null);
        //找到所有父ID为0的一级分类
        //组装成父子结构
        return categoryList.stream()
                .filter((categoryStream) -> categoryStream.getParentCid() == 0)// 这里首先使用java8的流处理对集合进行过滤，找到一级分类
                .peek((categoryStream) -> categoryStream.setChildren(getChildren(categoryStream, categoryList))) //peek也是个中间操作，执行给定操作并原路返回，也就是链式调用 categoryStream就是个瞎起的局部变量名，代表具体流元素
                // .sorted(Comparator.comparingInt(CategoryEntity::getSort)) // 利用实体类自有排序字段进行排序
                .sorted(Comparator.comparingInt(categoryStream -> (categoryStream.getSort() == null ? 0 : categoryStream.getSort())))
                .collect(Collectors.toList());
    }

    @Override
    public void removeMenuByIdS(List<Long> list) {
        // TODO 1,需要检查当前删除的菜单是否被其他地方引用
        //
        baseMapper.deleteBatchIds(list);
    }

    /*
     * @Author Chang
     * @Date 2024/3/4 10:20
     * @Description 找到catelogId的完整路径，用于前端修改时页面级联菜单的三级分类回显
     * @Param []
     * @Return java.lang.Long[]
     * @Since version 1.0
     */
    @Override
    public Long[] findCateLogPath(Long catelogId) {
        List<Long> paths = new ArrayList<>();
        List<Long> parentPath = this.findParentPath(catelogId, paths);
        // Collections.reverse(parentPath); // 因为初始添加顺序为 三级,二级,一级,而前端需要的参数顺序相反,所以需要逆序一下
        return parentPath.toArray(new Long[parentPath.size()]); // 类型转换 将list数组转换为long数组
    }

    /**
     * @param: [category] 缓存注解使用字符串需要添加单引号
     * @return: void
     * @author changzhenxi
     * @date 2024/9/10 - 14:35
     * @history 2024/9/10 - 14:35 changzhenxi create
     */
    // 组合操作 同时触发多个删除缓存操作
//    @Caching(evict = {
//            @CacheEvict(value = {"category"}, key = "'getLevelOneCategorys'"),
//            @CacheEvict(value = {"category"}, key = "'getCatelogJsonFromRedis'")
//    })
    @CacheEvict(value = {"category"}, allEntries = true) // 失效模式 直接删除整个category缓存分区的操作
    // @CachePut()  // 双写模式,进行数据更新的时候除了写到数据库中之外,再往缓存中写一份
    // @CacheEvict(value = {"category"}, key = "'getLevelOneCategorys'")  // 更新操作时清除缓存
    @Override
    @Transactional
    public void updateDetail(CategoryEntity category) {
        this.updateById(category);
        categoryBrandRelationService.updateCategory(category.getCatId(), category.getName());
    }

    /*
     * @Date 2024/8/20 09:35
     * @Author changzhenxi
     * @Description  每一个需要缓存的数据都来指定要放到哪个名字的缓存中  进行分区 分区并不是指redis中特定区域,而是spring给的一个标识
     *    如果配置文件中默认使用前缀,而又没有指定前缀,则会默认以分区名为前缀
     *  @Cacheable代表当前方法的结果需要缓存  如果缓存中有,则直接不再调用方法 反之则调用方法,并将结果放进缓存
     *   key默认自动生成  格式为 category::SimpleKey []  并且value默认使用java序列化机制,将序列化后的数据存到redis
     *  默认时间为-1(永不过期)
     *
     * 自定义设置选项:
     *  可以指定缓存使用的key key = "'LevelOneCategorys'" 如果是单纯的字符串则需要双引号内加单引号,
     *  否则会被当作表达式  key = "#root.method.name"则为设置方法名作为key
     *  指定缓存过期时间   application.properties中设置spring.cache.redis.time-to-live= 180000 毫秒单位
     *  将value设置为json格式
     *
     *  @Cacheable注解原理：
     *   springCache的cacheManager(redisCacheManager) -> cache(redisCache) -> cache负责缓存的读写
     *
     **/
    @Cacheable(value = {"category"}, key = "#root.method.name", sync = true)
    @Override
    public List<CategoryEntity> getLevelOneCategorys() {
        log.info("获取一级分类对象.....");
        long l = System.currentTimeMillis();
        List<CategoryEntity> list = list(new LambdaQueryWrapper<CategoryEntity>().eq(CategoryEntity::getParentCid, 0));
        long l1 = System.currentTimeMillis() - l;
        System.out.println("实际查询耗时为:  " + l1);
        return list;

    }

    @Override
    @Cacheable(value = {"category"}, key = "#root.method.name")
    public Map<String, List<CatelogTwoVo>> getCatelogJsonFromRedis() {
        System.out.println("开始查询数据库~~~~~~~~~~");
        List<CategoryEntity> categoryList = list();
        List<CategoryEntity> categorys = getChildLevelList(categoryList, 0L);
        Map<String, List<CatelogTwoVo>> finalMap = categorys.stream().collect(Collectors.toMap(
                categoryKey -> categoryKey.getCatId().toString(),
                categoryValue -> {
                    List<CategoryEntity> levelTwoList = getChildLevelList(categoryList, categoryValue.getCatId());
                    List<CatelogTwoVo> catelogTwoVos = null;
                    if (levelTwoList != null) {
                        catelogTwoVos = levelTwoList.stream().map(levelTwoCategory -> {
                            CatelogTwoVo catelogTwoVo = new CatelogTwoVo();
                            catelogTwoVo.setCatelogOneId(levelTwoCategory.getParentCid().toString());
                            catelogTwoVo.setId(levelTwoCategory.getCatId().toString());
                            catelogTwoVo.setName(levelTwoCategory.getName());
                            List<CategoryEntity> levelThreeCategoryList = getChildLevelList(categoryList, levelTwoCategory.getCatId());
                            if (levelThreeCategoryList != null) {
                                List<CatelogTwoVo.CatelogThreeVo> catelogThreeVos = levelThreeCategoryList.stream().map(levelThreeCategory ->
                                        new CatelogTwoVo.CatelogThreeVo(
                                                levelThreeCategory.getParentCid().toString(),
                                                levelThreeCategory.getCatId().toString(),
                                                levelThreeCategory.getName())).toList();
                                catelogTwoVo.setCatelogThreeList(catelogThreeVos);
                            }
                            return catelogTwoVo;
                        }).toList();
                    }
                    return catelogTwoVos;
                }));
        return finalMap;
    }


    // 低版本可能会出现堆外内存溢出 springboot2.0之后redis默认使用lettuce作为redis的客户端,而lettuce又会默认以netty进行网络通信
    // 主要就是lettuce的bug netty如果没有指定堆外内存,就会默认使用虚拟机配置的-Xmx XXX MB  作为堆外内存,
    // 只调大虚拟机的配置内存无法彻底解决,真正的解决方案:1,升级lettuce客户端 2,使用jedis
    public Map<String, List<CatelogTwoVo>> getCatelogJsonFromRedis2() throws InterruptedException {
        String catelogJson = RedisUtils.get("catelogJson"); // 从redis查出来的只是json字符串
        if (StringUtils.hasLength(catelogJson)) {
            System.out.println("======================缓存命中,直接返回======================");
            Map<String, List<CatelogTwoVo>> catelogMap = JSON.parseObject(catelogJson, new TypeReference<Map<String, List<CatelogTwoVo>>>() {
            });
            return catelogMap;
        }
        System.out.println("======================缓存未命中,尝试获取分布式锁查询数据库======================");
        // 加入缓存逻辑
        return getCatelogJsonFromDbWithRedisLock();
    }

    public Map<String, List<CatelogTwoVo>> getCatelogJsonFromDbWithRedisLock() throws InterruptedException {
        String randomValue = UUID.randomUUID().toString();
        String key = "lock";

        Map<String, List<CatelogTwoVo>> dataFromDB = null;
        // 占用分布式锁 这里的setnx方法是如果键不存在才创建,并且创建的同时就必须设置过期时间(必须是原子性操作),防止分布式锁无法释放,出现死锁情况
        System.out.println("加锁之前获取锁状态::::RedisUtils.get(key) = " + RedisUtils.get(key));
        boolean setnx = RedisUtils.setnx(key, randomValue, 300, TimeUnit.SECONDS); // 尝试加锁
        System.out.println("setnx = " + setnx);
        while (!setnx) {
            System.out.println("获取分布式锁失败,自旋等待......");
            Thread.sleep(500);
            setnx = RedisUtils.setnx(key, randomValue, 300, TimeUnit.SECONDS); // 尝试加锁
        }

        System.out.println("成功获取redis分布式锁");
        try {
            dataFromDB = getDataFromDB();
        } finally { // 无论当前是否执行成功,都不能影响锁的释放
            // redis的删除数据操作，也就是释放分布式锁操作跟获取锁一样都必须是原子性操作
            /*if (RedisUtils.get("lock").equals(uuid)){
                RedisUtils.del("lock"); //
            }
            return dataFromDB;*/
            // 这里不能使用常规的del操作，应该采用redis推荐的lua脚本进行原子性操作
            String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";

            Long execute = RedisUtils.executeLuaScript(luaScript, key, randomValue);
            System.out.println("脚本注入成功~  execute = " + execute);
            if (execute != 0) {
                System.out.println("Redis分布式锁已释放,等待其他线程获取.....");
            } else {
                System.out.println("非当前线程的锁,释放失败,当前redis库中线程锁randomValue为:" + RedisUtils.get(key) + "/t" + "当前线程锁为:" + randomValue);
            }
        }

        return dataFromDB;
    }

    private Map<String, List<CatelogTwoVo>> getDataFromDB() {
        String catelogJson = RedisUtils.get("catelogJson"); // 从redis查出来的只是json字符串
        if (StringUtils.hasLength(catelogJson)) {
            System.out.println("======================获取锁后再次查询redis,发现存在数据,直接返回======================");
            Map<String, List<CatelogTwoVo>> catelogMap = JSON.parseObject(catelogJson, new TypeReference<Map<String, List<CatelogTwoVo>>>() {
            });
            return catelogMap;
        }
        List<CategoryEntity> categoryList = list();
        List<CategoryEntity> categorys = getChildLevelList(categoryList, 0L);
        Map<String, List<CatelogTwoVo>> finalMap = categorys.stream().collect(Collectors.toMap(
                categoryKey -> categoryKey.getCatId().toString(),
                categoryValue -> {
                    List<CategoryEntity> levelTwoList = getChildLevelList(categoryList, categoryValue.getCatId());
                    List<CatelogTwoVo> catelogTwoVos = null;
                    if (levelTwoList != null) {
                        catelogTwoVos = levelTwoList.stream().map(levelTwoCategory -> {
                            CatelogTwoVo catelogTwoVo = new CatelogTwoVo();
                            catelogTwoVo.setCatelogOneId(levelTwoCategory.getParentCid().toString());
                            catelogTwoVo.setId(levelTwoCategory.getCatId().toString());
                            catelogTwoVo.setName(levelTwoCategory.getName());
                            List<CategoryEntity> levelThreeCategoryList = getChildLevelList(categoryList, levelTwoCategory.getCatId());
                            if (levelThreeCategoryList != null) {
                                List<CatelogTwoVo.CatelogThreeVo> catelogThreeVos = levelThreeCategoryList.stream().map(levelThreeCategory ->
                                        new CatelogTwoVo.CatelogThreeVo(
                                                levelThreeCategory.getParentCid().toString(),
                                                levelThreeCategory.getCatId().toString(),
                                                levelThreeCategory.getName())).toList();
                                catelogTwoVo.setCatelogThreeList(catelogThreeVos);
                            }
                            return catelogTwoVo;
                        }).toList();
                    }
                    return catelogTwoVos;
                }));
        String catelogJsonString = JSON.toJSONString(finalMap);
        RedisUtils.set("catelogJson", catelogJsonString, 3, TimeUnit.MINUTES); // 三分钟过期
        System.out.println("已查询数据库并存入redis缓存--------------");
        return finalMap;
    }

    // 从数据库查询封装  只要是同一把锁，就能锁住需要这个锁的所有线程  springboot所有的组件在容器中默认都是单例模式存在的
    /*public synchronized Map<String, List<CatelogTwoVo>> getCatelogJsonFromDbWithLocalLock() {
        // Map<String, List<CatelogTwoVo>> catelogJson = (Map<String, List<CatelogTwoVo>>) cacheMap.get("CatelogJson");

        // TODO 注意,不论是synchronized还是JUC包下的Lock锁.都是本地锁,只能锁当前容器中的对象.分布式集群中每一个相同的服务都是单独的容器,需要使用分布式锁

        // 在redis中如果不存在数据,那么所有的请求都会获取这把锁访问数据库,因为单例模式的存在,整个容器中CategoryServiceImpl对象只有一个,每一个线程都会排队拿锁
        // 如果第一个人从数据库获取数据之后释放掉锁,其他的线程就不应该再去查数据库了,因为数据已经被第一个线程查出来放到redis中了
        // 所以此处要做一个校验,为的就是拿到锁之后还需要再到redis中检查一遍.防止重复查库


        String catelogJson = RedisUtils.get("catelogJson"); // 从redis查出来的只是json字符串
        if (StringUtils.hasLength(catelogJson)) {
            System.out.println("======================获取锁后再次查询redis,发现存在数据,直接返回======================");
            Map<String, List<CatelogTwoVo>> catelogMap = JSON.parseObject(catelogJson,new TypeReference<Map<String, List<CatelogTwoVo>>>(){});
            return catelogMap;
        }
        System.err.println("<--------------------------------------------redis无缓存,开始获取锁访问数据库-------------------------------------------->");

            // 查询优化 直接查询所有然后循环挑选需要的数据，避免循环查库严重影响吞吐量
            // 查询所有商品分类
            List<CategoryEntity> categoryList = list();
            // 获取所有一级分类
            List<CategoryEntity> categorys = getChildLevelList(categoryList, 0L);
            // 封装json key为一级分类id value为一级分类中的二级分类对象，其中二级分类对象也包含各自的三级分类集合
            Map<String, List<CatelogTwoVo>> finalMap = categorys.stream().collect(Collectors.toMap(
                    categoryKey -> categoryKey.getCatId().toString(),
                    // 查询二级分类及三级分类分类并封装
                    categoryValue -> {
                        // 一级分类的id就是二级分类的父id，以此为条件筛选出全部商品分类集合中的所有二级分类
                        List<CategoryEntity> levelTwoList = getChildLevelList(categoryList, categoryValue.getCatId());

                        List<CatelogTwoVo> catelogTwoVos = null;
                        if (levelTwoList != null) {
                            catelogTwoVos = levelTwoList.stream().map(levelTwoCategory -> {
                                //封装
                                CatelogTwoVo catelogTwoVo = new CatelogTwoVo();
                                catelogTwoVo.setCatelogOneId(levelTwoCategory.getParentCid().toString());
                                catelogTwoVo.setId(levelTwoCategory.getCatId().toString());
                                catelogTwoVo.setName(levelTwoCategory.getName());
                                // 同理筛选出全部商品分类集合中的所有三级分类
                                List<CategoryEntity> levelThreeCategoryList = getChildLevelList(categoryList, levelTwoCategory.getCatId());
                                if (levelThreeCategoryList != null) {
                                    List<CatelogTwoVo.CatelogThreeVo> catelogThreeVos = levelThreeCategoryList.stream().map(levelThreeCategory ->
                                            new CatelogTwoVo.CatelogThreeVo(
                                                    levelThreeCategory.getParentCid().toString(),
                                                    levelThreeCategory.getCatId().toString(),
                                                    levelThreeCategory.getName())).toList();
                                    catelogTwoVo.setCatelogThreeList(catelogThreeVos);
                                }
                                return catelogTwoVo;

                            }).toList();
                        }

                        return catelogTwoVos;
                    }));
        // 第一个线程拿到锁查询数据库后,往redis存数据的将存未存之时,锁可能已经被释放了
        // 而高并发的情况下,第二个线程拿到锁查redis,但是第一个线程的数据还没来得及往redis里面放,就会认为redis中没有数据,再去查一遍数据库
        // 所以需要增大锁的粒度.查询数据库之后直接往redis放,将添加redis缓存的步骤也放在锁中,这样就能确保过期之前只查一遍数据库
        String catelogJsonString = JSON.toJSONString(finalMap);
        RedisUtils.set("catelogJson", catelogJsonString, 3, TimeUnit.MINUTES); // 三分钟过期
        return finalMap;
    }*/

    // 不再查库，而是通过筛选的方式 以空间换时间 以磁盘内存换取查询数据的响应时间
    private List<CategoryEntity> getChildLevelList(List<CategoryEntity> categoryList, Long parentCid) {
        return categoryList.stream().filter(categoryEntity -> categoryEntity.getParentCid().equals(parentCid)).toList();
        // return list(new LambdaQueryWrapper<CategoryEntity>().eq(CategoryEntity::getParentCid, categoryValue.getCatId()));
    }

    private List<Long> findParentPath(Long catelogId, List<Long> paths) {

        CategoryEntity categoryEntity = this.getById(catelogId);
        if (categoryEntity.getParentCid() != 0) {
            findParentPath(categoryEntity.getParentCid(), paths); //递归查询 将自身节点以及父节点和往上的节点id都添加到paths集合中
        }
        paths.add(catelogId);
        return paths;
    }

    // 递归查找所有菜单的子菜单
    private List<CategoryEntity> getChildren(CategoryEntity category, List<CategoryEntity> allList) {
        return allList.stream()
                .filter((categoryStream) -> Objects.equals(categoryStream.getParentCid(), category.getCatId())) // 找到父id与一级分类id相同的二级分类
                .peek((categoryStream) -> categoryStream.setChildren(this.getChildren(categoryStream, allList)))// 二级分类本身也有三级分类集合，所以调用自身进行递归操作
                .sorted(Comparator.comparingInt(categoryStream -> (categoryStream.getSort() == null ? 0 : categoryStream.getSort()))) // 因为数据库sort字段可能为空,所以要加一个用三元运算符添加一个默认值
                .collect(Collectors.toList());
    }

}